package cs304.p3.library.service;

import java.sql.SQLException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import cs304.p3.library.domain.BookCopy;
import cs304.p3.library.domain.BookCopy.BookStatus;
import cs304.p3.library.domain.Borrower;
import cs304.p3.library.domain.Borrowing;
import cs304.p3.library.domain.Fine;
import cs304.p3.library.domain.HoldRequest;
import cs304.p3.library.domain.ReturnBook;
import cs304.p3.library.exceptions.AddBorrowerException;
import cs304.p3.library.exceptions.AddBorrowingException;
import cs304.p3.library.exceptions.InvalidBorrowerException;
import cs304.p3.library.exceptions.UnavailableBookException;
import cs304.p3.library.util.SqlUtils;

public class ClerkService extends Service {
	private static final long WEEKS_TO_MS = 7*24*60*60*1000;
	// Output: A new borrower id and a password, or "Cancelled"
	public Borrower addBorrower(String name, String address, String phone, String emailAddress,
			String sinOrStNo, Date expiryDate, String type) throws AddBorrowerException {

		Borrower borrower = null;
		String bid = null;
		String password = null;

		try {
			bid = borrowerDao.generateBid();
			password = borrowerDao.generatePassword();
		} catch(SQLException e) {
			String error = "Generating new bid/password failed.";
			System.err.println(e.getMessage());
			throw new AddBorrowerException(error);
		}

		try {
			borrowerDao.insertBorrower(bid, password, name, address, phone, emailAddress,
					sinOrStNo, expiryDate, type);
		} catch(SQLException e) {
			try {
				String error = "Adding new borrower failed; rolling back.";
				System.err.println(error);
				System.err.println(e.getMessage());
				connection.rollback();
				throw new AddBorrowerException(error);
			} catch(SQLException e2) {
				e2.printStackTrace();
				throw new RuntimeException();
			}
		}

		borrower = new Borrower(bid, password, name, address, phone, emailAddress, sinOrStNo,
				expiryDate, type);
		return borrower;
	}

	public List<BookCopy> checkOut(String bid, List<String> callNumbers)
			throws InvalidBorrowerException, UnavailableBookException, AddBorrowingException {
		Map<String, String> keyVals = Maps.newHashMap();
		keyVals.put("bid", bid);
		List<Borrower> borrowers;
		Borrower borrower;
		try {
			borrowers = borrowerDao.selectBorrower(keyVals);
			if(borrowers.isEmpty())
				throw new InvalidBorrowerException(bid);
			else
				borrower = borrowers.get(0);

		} catch(SQLException e) {
			System.err.println("Clerk.checkOut : Borrower is invalid");
			throw new InvalidBorrowerException(bid);
		}

		//borrower is valid at this point

		List<String> unavailableBooks = Lists.newArrayList();
		List<BookCopy> books = Lists.newArrayList();
		for(String callNumber: callNumbers) {
			try {
				List<BookCopy> copies = bookDao.selectBookCopy(callNumber, SqlUtils.ANY_VALUE,
						BookStatus.IN.toString());
				if(copies.isEmpty())
					unavailableBooks.add(callNumber);
				else
					books.add(copies.get(0));
			} catch(SQLException e) {
				unavailableBooks.add(callNumber);
			}
		}

		if(!unavailableBooks.isEmpty()) {
			throw new UnavailableBookException(unavailableBooks);
		}

		// bid and all callNumbers are valid at this point
		long borrowingLimit = 0;
		try {
			borrowingLimit = borrowerDao.selectBorrowerTypes().get(borrower.getType()) * WEEKS_TO_MS;
		} catch(SQLException e1) {
			e1.printStackTrace();
		}

		Date now = new Date();

		try {
			connection.setAutoCommit(false);
			for(BookCopy book: books) {
				UUID id = UUID.randomUUID();
				String borid = Long.toHexString(id.getMostSignificantBits())
						+ Long.toHexString(id.getLeastSignificantBits());			
				book.setOutDate(now);
				Date dueDate = new Date(now.getTime() + borrowingLimit);
				book.setDueDate(dueDate);

				bookDao.insertBorrowing(borid, bid, book.getCallNumber(), book.getCopyNo(),SqlUtils.getDateString(book.getOutDate()), SqlUtils.getDateString(dueDate));
				bookDao.updateBookCopy(book.getCallNumber(), book.getCopyNo(),BookStatus.OUT.toString());
			}
			connection.commit();
			connection.setAutoCommit(true);
		}
		catch(SQLException e) {
			System.out.println(e.getMessage());
			try {
				connection.rollback();
			} catch(SQLException e1) {
				throw new RuntimeException();
			}
			System.out.println(e.getMessage());
			throw new AddBorrowingException();
		}

		return books;
	}

	/**
	 * Processes a return. When an item is returned, the clerk records the return<br>
	 * by providing the item's catalogue number. The system determines<br>
	 * the borrower who had borrowed the item and records that the the item is "in"
	 */
	public ReturnBook processReturn(String callNumber, int copyNo) throws InvalidBorrowerException,
	UnavailableBookException {
		Date now = new Date();
		List<Borrowing> books = null;
		Borrowing book = null;
		// select borrowing
		try {
			books = bookDao.selectBorrowingFromBookCopy(callNumber, copyNo);
			if(books.isEmpty())
				throw new UnavailableBookException(Lists.newArrayList(callNumber));
			else
				book = books.get(0);
		} catch(SQLException e2) {
			throw new UnavailableBookException(Lists.newArrayList(callNumber));
		}

		// select borrower
		Borrower borrower = null;
		List<Borrower> borrowers;
		try {
			Map<String, String> keyVals = Maps.newHashMap();
			keyVals.put("bid", book.getBid());
			borrowers = borrowerDao.selectBorrower(keyVals);
			if(borrowers.isEmpty())
				throw new InvalidBorrowerException(book.getBid());
			else
				borrower = borrowers.get(0);

		} catch(SQLException e) {
			System.err.println("Clerk.processReturn : Borrower is invalid");
			throw new InvalidBorrowerException(book.getBid());
		}

		// borrowing and borrower exist at this point

		// check if there's a hold request
		List<HoldRequest> holdRequests = Lists.newArrayList();
		try {
			holdRequests = holdRequestDao.selectHoldRequest(callNumber);
		} catch(SQLException e) {
			e.printStackTrace();
		}

		HoldRequest oldestRequest = null;
		if(!holdRequests.isEmpty()) {
			// find oldest request
			long min = Long.MAX_VALUE;
			for(HoldRequest hr: holdRequests) {
				if(hr.getIssuedDate().getTime() < min)
					oldestRequest = hr;
			}
		}

		//if there is a hold request, retrieve the borrower info
		Borrower holder = null;
		try {
			holder = borrowerDao.selectBorrower(oldestRequest.getBid(), SqlUtils.ANY_VALUE);
		} catch (SQLException e2) {
			// TODO Auto-generated catch block
			e2.printStackTrace();
		}

		oldestRequest.setBorrower(holder);

		// updates
		// 1. update book copy status to in or on hold
		// 2. delete the hold request
		try {
			String bookStatus = oldestRequest == null ? BookStatus.IN.toString() : BookStatus.ON_HOLD.toString();
			connection.setAutoCommit(false);
			bookDao.updateBookCopy(callNumber, copyNo, bookStatus);
			if(oldestRequest != null) 
				holdRequestDao.deleteHoldRequest(oldestRequest.getHid());
			connection.commit();
			connection.setAutoCommit(true);
		} catch(SQLException e) {
			try {
				connection.rollback();
			} catch(SQLException e1) {
				throw new RuntimeException();
			}
		}

		Fine fine = null;
		// check if overdue
		if(now.getTime() > book.getInDate().getTime()) {
			UUID id = UUID.randomUUID();
			String fid = Long.toHexString(id.getMostSignificantBits())
					+ Long.toHexString(id.getLeastSignificantBits());
			try {
				fineDao.insertFine(fid, 100, SqlUtils.getDateString(now), SqlUtils.NULL,
						book.getBorid());
				fine = new Fine();
				fine.setAmount(100);
				fine.setBorid(book.getBorid());
				fine.setBorrower(borrower);
				fine.setFid(fid);
				fine.setIssuedDate(now);
				// TODO: init fine
			} catch(SQLException e) {
				try {
					connection.rollback();
				} catch(SQLException e1) {
					throw new RuntimeException();
				}
			}
		}

		return new ReturnBook(fine, oldestRequest);
	}

	/**
	 * Checks for overdue BookCopies and returns a list of these copies.
	 * @return
	 */
	public List<BookCopy> checkOverdue() {
		try {
			return bookDao.selectCurrentlyBorrowedBooks("");
		} catch(SQLException e) {
			try {
				System.err.println("Checking for overdue items failed; rolling back.");
				connection.rollback();
			} catch(SQLException e1) {
				throw new RuntimeException();
			}
		}
		return null;
	}

	/**
	 * extra service for UI
	 */
	public Map<String, Integer> getBorrowerTypes() {
		try {
			return borrowerDao.selectBorrowerTypes();
		} catch(SQLException e) {
			e.printStackTrace();
		}
		return null;
	}
}
